Imports System.Drawing
Imports System.Drawing.Drawing2D

Namespace Annotations

  ''' <summary>
  ''' Set of related annotations. Together they define a region.
  ''' </summary>
  ''' <remarks></remarks>
  Public Class AnnotationContainer
    Inherits CollectionWithEventsTemplate(Of Annotation)
    Implements ICloneable, IDisposable

    'Public Shared NormalOpacity As Integer = 64
    'Public Shared SelectedOpacity As Integer = 196

    Private _needRegionUpdate As Boolean = True

#Region "Events"
    ''' <summary>
    ''' Raised when an annotation changes
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <remarks></remarks>
    Public Event AnnotationChanged(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
    Private Sub OnAnnotationChanged(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
      If e.Type <> CollectionChangedEventArgs.EventType.PropertySet Then Me._needRegionUpdate = True
      If Me.Events Then RaiseEvent AnnotationChanged(sender, e)
    End Sub

    ''' <summary>
    ''' Overide the base class event handler in order to make sure the cached region is updated when needed. 
    ''' This is more efficient than handling the base class event itself.
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Protected Friend Overrides Sub OnChangedEvent(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
      If e.Type <> CollectionChangedEventArgs.EventType.PropertySet Then Me._needRegionUpdate = True
      MyBase.OnChangedEvent(sender, e)
    End Sub
#End Region

#Region "Constructor - Destructor"
    ''' <summary>
    ''' Create a new annotation container.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
      MyBase.New()
      Debug.WriteLine("AnnotationContainer.New")
    End Sub

    Public Overridable Overloads Sub Dispose() Implements IDisposable.Dispose
      Dispose(True)
      GC.SuppressFinalize(Me)
    End Sub

    Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
      'We clean up, the program leaks memory otherwise due to unmanaged GDI+ objects
      Debug.WriteLine("AnnotationContainer.Dispose")
      If disposing Then
        ' Free other state (managed objects).
        MyBase.Clear()
      End If
    End Sub

#End Region

#Region "Properties"

    Friend _key As String
    ''' <summary>
    ''' The key by which the annotation is known in its parent collection.
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public ReadOnly Property Key() As String
      Get
        Return _key
      End Get
    End Property

    Protected Friend _parent As Annotations
    ''' <summary>
    ''' The parent of the collection object.
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly Property Parent() As Annotations
      Get
        Return Me._parent
      End Get
    End Property

    ''' <summary>
    ''' Obtain or set item with the given key. If the key does not exist a new item is created when setting.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overloads Property Annotation(ByVal key As String) As Annotation
      Get
        Dim ann As Annotation = Nothing
        MyBase.TryGetValue(key, ann)
        Return ann
      End Get
      Set(ByVal value As Annotation)
        InitializeAnnotation(key, value)
        MyBase.Item(key) = value
      End Set
    End Property

    Public Overloads ReadOnly Property Annotation(ByVal index As Integer) As Annotation
      Get
        Return MyBase.Item(index)
      End Get
    End Property

    ''' <summary>
    ''' Get an item
    ''' </summary>
    ''' <param name="key"></param>
    ''' <value></value>
    ''' <remarks>Not very clear to use this in code, use 'Annotation' property instead. We cannot seem to hide this effectively ...
    ''' </remarks>
    Public Shadows Property Item(ByVal key As String) As Annotation
      Get
        Return MyBase.Item(key)
      End Get
      Set(ByVal value As Annotation)
        Me.Annotation(key) = value
      End Set
    End Property
#End Region

#Region "Drawing properties"

    Private _color As System.Drawing.Color = Drawing.Color.Red
    ''' <summary>
    ''' The color used to draw the regions
    ''' </summary>
    ''' <value></value>
    ''' <remarks>Do not include opacity in this color!
    ''' </remarks>
    Public Property Color() As System.Drawing.Color
      Get
        Return _color
      End Get
      Set(ByVal Value As System.Drawing.Color)
        _color = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _opacity As Integer = 64
    ''' <summary>
    ''' The opacity of a region when not selected
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Opacity() As Integer
      Get
        Return _opacity
      End Get
      Set(ByVal value As Integer)
        _opacity = CInt(Math.Max(Math.Min(255, value), 0))
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property


    Private _selectedOpacity As Integer = 196
    ''' <summary>
    ''' The opacity of a region when not selected
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property SelectedOpacity() As Integer
      Get
        Return _selectedOpacity
      End Get
      Set(ByVal value As Integer)
        _selectedOpacity = CInt(Math.Max(Math.Min(255, value), 0))
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _regionVisible As Boolean = True
    ''' <summary>
    ''' Show or hide the region that is formed by combining all the annotations in this container. 
    ''' </summary>
    ''' <value></value>
    ''' <remarks>Drawing the region is computationally quite expensive
    ''' </remarks>
    Public Property RegionVisible() As Boolean
      Get
        Return _regionVisible
      End Get
      Set(ByVal Value As Boolean)
        _regionVisible = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property


    Private _text As String
    ''' <summary>
    ''' The text drawn near the first point of the annotation
    ''' </summary>
    ''' <value></value>
    ''' <remarks>
    ''' </remarks>
    Public Property Text() As String
      Get
        Return _text
      End Get
      Set(ByVal Value As String)
        _text = Value
        Me.OnChangedEvent(Me, New CollectionChangedEventArgs(CollectionChangedEventArgs.EventType.PropertySet))
      End Set
    End Property

    Private _region As Region
    ''' <summary>
    ''' The region made by combining the regions of all the annotations of this container
    ''' </summary>
    ''' <value></value>
    ''' <remarks>Do not modify this region!!!!!
    ''' </remarks>
    Private ReadOnly Property Region() As Region
      Get
        Me.UpdateRegion()
        Return _region
      End Get
    End Property

#End Region

#Region "Methods"

    ''' <summary>
    ''' Sets the color used to draw the lines, points and text of all the annotations.
    ''' </summary>
    ''' <param name="value" ></param >
    ''' <remarks>When set, changes properties of all annotations in this container.</remarks>
    Public Sub SetColor(ByVal value As System.Drawing.Color)
      For Each ann As Annotation In Me
        ann._color = Value
      Next
    End Sub

    ''' <summary>
    ''' Sets the point size used to draw the points of all the annotations.
    ''' </summary>
    ''' <param name="value" ></param >
    ''' <remarks>Changes properties of all annotations in this container.
    ''' </remarks>
    Public Sub SetPointSize(ByVal value As Integer)
      For Each ann As Annotation In Me
        ann._pointSize = value
      Next
    End Sub


    ''' <summary>
    ''' Sets the line width used to draw the lines of all the annotations.
    ''' </summary>
    ''' <param name="value" ></param >
    ''' <remarks>Changes properties of all annotations in this container.
    ''' </remarks>
    Public Sub SetLineWidth(ByVal value As Integer)
      For Each ann As Annotation In Me
        ann._lineWidth = value
      Next
    End Sub

    ''' <summary>
    ''' Get a copy of the region made by combining the regions of all the polygons of this set
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Function GetRegion() As Region
      Dim rgn As New Region
      rgn.MakeEmpty()
      For Each ann As Annotation In Me
        Util.CombineRegions(rgn, ann.GetRegion, ann.AddMode)
      Next
      Return rgn
    End Function

    ''' <summary>
    ''' Add a annotation to the collection, and connect event handlers to it.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <param name="value"></param>
    ''' <remarks></remarks>
    Public Overrides Sub Add(ByVal key As String, ByVal value As Annotation)
      InitializeAnnotation(key, value)
      MyBase.Add(key, value)
    End Sub

    ''' <summary>
    ''' Return a copy of the annotation set.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function Clone() As Object Implements ICloneable.Clone
      Dim ann As New AnnotationContainer()
      ann.AddRange(Me)
      Return ann
    End Function

    ''' <summary>
    ''' Scale all polygons inside this set.
    ''' </summary>
    ''' <param name="sScale"></param>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Sub Scale(ByVal sScale As Double)
      For Each ann As Annotation In Me
        ann.Scale(sScale)
      Next
    End Sub

    ''' <summary>
    ''' Scale all polygons inside this set.
    ''' </summary>
    ''' <param name="scale"></param>
    ''' <param name="refPt"></param>
    ''' <remarks></remarks>
    Public Sub Scale(ByVal scale As Double, ByVal refPt As PointF)
      For Each ann As Annotation In Me
        ann.Scale(scale, refPt)
      Next
    End Sub

    ''' <summary>
    ''' Move all polygons inside this set
    ''' </summary>
    ''' <param name="dx"></param>
    ''' <param name="dy"></param>
    Public Sub Move(ByVal dx As Single, ByVal dy As Single)
      For Each ann As Annotation In Me
        ann.Move(dx, dy)
      Next
    End Sub

    ''' <summary>
    ''' Quantize all polygons inside this set
    ''' </summary>
    ''' <param name="sMinDist"></param>
    ''' <param name="sMaxDist"></param>
    ''' <remarks></remarks>
    Public Sub Quantize(ByVal sMinDist As Double, ByVal sMaxDist As Double)
      For Each ann As Annotation In Me
        ann.Quantize(sMinDist, sMaxDist)
      Next
    End Sub

    ''' <summary>
    ''' Expand all polygons inside this set
    ''' </summary>
    ''' <param name="distance"></param>
    ''' <remarks></remarks>
    Public Overloads Sub Expand(ByVal distance As Single)
      For Each ann As Annotation In Me
        ann.Expand(distance)
      Next
    End Sub

    ''' <summary>
    ''' Rotate all polygons inside this set
    ''' </summary>
    ''' <param name="angle"></param>
    ''' <param name="refPt"></param>
    ''' <remarks></remarks>
    Public Sub Rotate(ByVal angle As Double, ByVal refPt As PointF)
      For Each ann As Annotation In Me
        ann.Rotate(angle, refPt)
      Next
    End Sub

    ''' <summary>
    ''' Flip all polygons inside this set
    ''' </summary>
    ''' <param name="x"></param>
    ''' <remarks></remarks>
    Public Sub Flip(ByVal x As Double, ByVal axis As Integer)

      For Each ann As Annotation In Me
        ann.Flip(x, axis)
      Next
    End Sub

    ''' <summary>
    ''' Transform all polygons inside this set
    ''' </summary>
    ''' <param name="m"></param>
    ''' <remarks></remarks>
    Public Sub Transform(ByVal m As Matrix)
      For Each ann As Annotation In Me
        ann.Transform(m)
      Next
    End Sub

    ''' <summary>
    ''' Clip all polygons inside this set
    ''' </summary>
    ''' <param name="rect"></param>
    ''' <remarks></remarks>
    Public Sub Clip(ByVal rect As RectangleF)
      For Each ann As Annotation In Me
        ann.Clip(rect)
      Next
    End Sub

    Private Sub InitializeAnnotation(ByVal key As String, ByVal value As Annotation)
      AddHandler value.SelectedChanged, AddressOf OnSelectedChangedEvent
      AddHandler value.Changed, AddressOf OnAnnotationChanged

      'Inherit color from container if not set
      If value._color.IsEmpty Then value._color = Me._color

      value._key = key
      value._parent = Me
    End Sub

#If IMAGEPROCESSING Then
    Public Shared Function FromImage(ByVal mask As Bitmap, ByVal image As Bitmap) As AnnotationContainer
      Return FromImage(mask, CSng(image.Width / mask.Width))
    End Function

    ''' <summary>
    ''' Create a annotation set from a 1-bit image (mask).
    ''' </summary>
    ''' <param name="mask"></param>
    ''' <returns></returns>
    ''' <remarks>Masks are often undersampled so we need to pass on the scale of the polygons versus the mask</remarks>
    Public Shared Function FromImage(ByVal mask As Bitmap, ByVal scale As Single) As AnnotationContainer
      If mask.PixelFormat = Imaging.PixelFormat.Format1bppIndexed Then
        Dim maskBorderPts As Point()() = ImageProcessing.Image1.GetObjectBorderPoints(mask)
        Dim annContainer As New AnnotationContainer()

        For i As Integer = 0 To maskBorderPts.GetUpperBound(0)
          Dim ann As New Annotation()
          Dim maskPts() As Point = maskBorderPts(i)
          Dim polyPts(maskPts.Length - 1) As PointF


          For j As Integer = 0 To maskPts.Length - 1
            polyPts(j).X = maskPts(j).X * scale
            polyPts(j).Y = maskPts(j).Y * scale
          Next
          ann.AddRange(polyPts)
          annContainer.Add(i.ToString, ann)
        Next
        Return annContainer
      Else
        Throw New ArgumentException("Can only create an annotation containers from a 1 bit image")
        Return Nothing
      End If
    End Function
#End If

    ''' <summary>
    ''' Create an annotation container from an XML node in SVG format.
    ''' </summary>
    ''' <param name="node"></param>
    ''' <returns></returns>
    ''' <remarks>We add a few extra properties not normally found in SVG:
    ''' "fill-selected-opacity" for the opacity when selected, 
    ''' "fill-visible" for the region visibility.</remarks>
    Public Shared Function FromXML(ByVal node As Xml.XmlNode) As AnnotationContainer
      'Create containers, with ID as key
      Dim numberFormat As System.Globalization.NumberFormatInfo = System.Globalization.NumberFormatInfo.InvariantInfo

      Dim polySet As New AnnotationContainer
      polySet.Color = Util.WebColorToColor(node.Attributes.GetNamedItem("fill").Value)
      polySet.Opacity = CInt(Math.Round(Single.Parse(node.Attributes.GetNamedItem("fill-opacity").Value, numberFormat) * 255))

      'Some old file may not have these attributes.
      Dim optionalNode As Xml.XmlNode = node.Attributes.GetNamedItem("fill-visible")
      If Not optionalNode Is Nothing Then
        polySet.RegionVisible = CBool(optionalNode.Value)
      Else
        polySet.RegionVisible = True
      End If
      optionalNode = node.Attributes.GetNamedItem("fill-selected-opacity")
      If Not optionalNode Is Nothing Then
        polySet.SelectedOpacity = CInt(Math.Round(Single.Parse(optionalNode.Value, numberFormat) * 255))
      Else
        polySet.SelectedOpacity = polySet.Opacity + 128
      End If

      'Title = text. Text visible if text node present
      polySet.Text = Util.GetSVGText(node, "title")

      'Now look for g again, they represent annotations and contain a polyline.
      Dim oAGNodes As Xml.XmlNodeList = node.SelectNodes("g")
      Dim oAGNode As Xml.XmlNode
      For Each oAGNode In oAGNodes
        Dim ann As Annotation = CIPF.Annotations.Annotation.FromXML(oAGNode)

        'Old format SVG file has id node in polyline. Make sure we don't choke on old files
        Dim idNode As Xml.XmlNode = oAGNode.Attributes.GetNamedItem("id")
        If Not idNode Is Nothing Then
          polySet.Add(idNode.Value, ann)
        Else
          Dim oldIDNode As Xml.XmlNode = oAGNode.SelectSingleNode("annline")
          polySet.Add(oldIDNode.Attributes.GetNamedItem("id").Value, ann)
        End If
      Next

      Return polySet
    End Function

    ''' <summary>
    ''' Create XML fragment from current drawable annotation container
    ''' </summary>
    ''' <param name="SVG"></param>
    ''' <remarks></remarks>
    Public Sub ToXML(ByVal SVG As Xml.XmlWriter)
      Dim numberFormat As System.Globalization.NumberFormatInfo = System.Globalization.NumberFormatInfo.InvariantInfo
      SVG.WriteStartElement("g")

      SVG.WriteAttributeString("id", Me.Key)
      SVG.WriteAttributeString("fill", Util.ColorToWebColor(Me.Color))
      SVG.WriteAttributeString("fill-rule", "evenodd")

      'This will loose some info, namely the nominal opacity when regionVisible is false
      SVG.WriteAttributeString("fill-opacity", (Me.Opacity / 255.0).ToString(numberFormat))
      SVG.WriteAttributeString("fill-selected-opacity", (Me.SelectedOpacity / 255.0).ToString(numberFormat))
      SVG.WriteAttributeString("fill-visible", Me.RegionVisible.ToString)

      'Add text in title.
      If Not Me.Text Is Nothing Then
        SVG.WriteElementString("title", Me.Text)
        If Me.Count > 0 Then
          If Me.Annotation(0).Count > 0 Then
            Util.AddSVGText(SVG, Me.Text, Me.Annotation(0).Point(0), 12, Me.Color, numberFormat)
          End If
        End If
      End If

      'Write an annotation as polyline. We put it also in a group so that a title and other 
      'metadata can be added ...
      For Each ann As Annotation In Me
        ann.ToXML(SVG)
      Next
      SVG.WriteEndElement()
    End Sub

#End Region

#Region "Draw related methods"
    Private Sub UpdateRegion()
      If _needRegionUpdate Then
        Debug.WriteLine("Updating region ...", "AnnotationContainer.UpdateRegion")
        _region = Me.GetRegion()
        _needRegionUpdate = False
      End If
    End Sub

    Friend Sub Draw(ByVal g As Graphics, ByVal info As DrawingInfo)
      'Draw the AnnotationContainer, after the background image has been repainted
      'GetRegion is an expensive step, avoid for faster drawing!!!!
      'The text for an annotation container is actually positioned vs the first
      'point of the first annotation, and so it is drawn when the first annotation is drawn!
      Debug.WriteLine("Drawing AnnotationContainer " & Me.Key, "AnnotationContainer.Draw")

      'Compute the drawing info
      If Me.Visible = True And Me.Count > 0 Then

        'Show the region in desired color. Avoid coloring regions for fast drawing!!!!
        If _regionVisible Then
          If Not Region Is Nothing Then
            Dim b1 As SolidBrush
            If Me.Selected Then
              b1 = New SolidBrush(Color.FromArgb(_selectedOpacity, _color))
            Else
              b1 = New SolidBrush(Color.FromArgb(_opacity, _color))
            End If
            g.FillRegion(b1, _region)
            b1.Dispose()
          End If
        End If

        For Each ann As Annotation In Me
          ann.Draw(g, info, Selected, _text)
        Next
      End If
    End Sub
#End Region

  End Class
End Namespace

